package org.dromara.system.controller.monitor;

import cn.dev33.satoken.annotation.SaCheckPermission;
import cn.dev33.satoken.exception.NotLoginException;
import cn.dev33.satoken.stp.StpUtil;
import cn.hutool.core.bean.BeanUtil;
import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.CacheConstants;
import org.dromara.common.core.domain.R;
import org.dromara.common.core.domain.dto.UserOnlineDTO;
import org.dromara.common.core.utils.StreamUtils;
import org.dromara.common.core.utils.StringUtils;
import org.dromara.common.log.annotation.Log;
import org.dromara.common.log.enums.BusinessType;
import org.dromara.common.mybatis.core.page.TableDataInfo;
import org.dromara.common.redis.utils.RedisUtils;
import org.dromara.common.web.core.BaseController;
import org.dromara.system.domain.SysUserOnline;
import org.springframework.web.bind.annotation.*;

import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import java.util.stream.Collectors;


/**
 * @Author 刘武贵
 * @Date 2024/8/15 22:00
 * @Description 功能描述：在线用户监控
 */
@RequiredArgsConstructor
@RestController
@RequestMapping("/monitor/online")
public class SysUserOnlineController extends BaseController {

    /**
     * 获取在线用户监控列表
     * 本方法主要用于查询和过滤在线用户，可以根据IP地址和用户名进行筛选
     * 数据源为Redis中存储的用户会话信息，通过搜索未过期的token来获取用户信息
     *
     * @param ipaddr   IP地址
     * @param userName 用户名
     * @return 返回一个TableDataInfo对象，包含筛选后的在线用户列表
     */
    @SaCheckPermission("monitor:online:list")
    @GetMapping("/list")
    public TableDataInfo<SysUserOnline> list(String ipaddr, String userName) {
        // 获取所有未过期的 token
        List<String> keys = StpUtil.searchTokenValue("", 0, -1, false);
        List<UserOnlineDTO> userOnlineDTOList = new ArrayList<>();
        // 遍历所有 token，获取对应的用户在线信息
        for (String key : keys) {
            String token = StringUtils.substringAfterLast(key, ":");
            // 如果已经过期则跳过
            if (StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) < -1) {
                continue;
            }
            // 通过Redis缓存键获取用户在线信息并添加到列表
            userOnlineDTOList.add(RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token));
        }
        // 根据IP地址和用户名进行筛选
        if (StringUtils.isNotEmpty(ipaddr) && StringUtils.isNotEmpty(userName)) {
            userOnlineDTOList = StreamUtils.filter(userOnlineDTOList, userOnline ->
                StringUtils.equals(ipaddr, userOnline.getIpaddr()) &&
                    StringUtils.equals(userName, userOnline.getUserName())
            );
        } else if (StringUtils.isNotEmpty(ipaddr)) {
            // 仅根据IP地址进行筛选
            userOnlineDTOList = StreamUtils.filter(userOnlineDTOList, userOnline ->
                StringUtils.equals(ipaddr, userOnline.getIpaddr())
            );
        } else if (StringUtils.isNotEmpty(userName)) {
            // 仅根据用户名进行筛选
            userOnlineDTOList = StreamUtils.filter(userOnlineDTOList, userOnline ->
                StringUtils.equals(userName, userOnline.getUserName())
            );
        }
        // 列表倒序，以确保最近活跃的用户位于列表前端
        Collections.reverse(userOnlineDTOList);
        // 移除列表中所有null元素
        userOnlineDTOList.removeAll(Collections.singleton(null));
        // 将筛选后的UserOnlineDTO列表转换为SysUserOnline列表
        List<SysUserOnline> userOnlineList = BeanUtil.copyToList(userOnlineDTOList, SysUserOnline.class);
        // 构建并返回TableDataInfo对象
        return TableDataInfo.build(userOnlineList);
    }

    /**
     * 强退用户
     * 强制登出指定令牌的用户
     * 此方法通过令牌ID强制登出用户，常用于注销会话
     *
     *
     * @param tokenId token值 令牌ID，用于标识需要登出的用户会话
     * @return 登出操作结果，成功返回ok，失败返回error
     */
    @SaCheckPermission("monitor:online:forceLogout")
    @Log(title = "在线用户", businessType = BusinessType.FORCE)
    @DeleteMapping("/{tokenId}")
    public R<Void> forceLogout(@PathVariable String tokenId) {
        try {
            // 尝试通过令牌值踢出用户，实现登出功能
            StpUtil.kickoutByTokenValue(tokenId);
        } catch (NotLoginException ignored) {
            // 忽略未登录异常，因为这并不影响登出操作的成功
        }
        return R.ok();
    }

    /**
     * 获取当前用户登录在线设备
     * 该方法首先根据登录账号id获取对应的token集合，然后通过这些token从Redis中获取用户在线详情
     * 主要的步骤包括：获取token列表、过滤有效token、从Redis获取每个token对应的在线用户信息、以及数据转换和封装
     *
     * @return 返回TableDataInfo对象，包含用户在线列表
     */
    @GetMapping()
    public TableDataInfo<SysUserOnline> getInfo() {
        // 获取指定账号 id 的 token 集合
        List<String> tokenIds = StpUtil.getTokenValueListByLoginId(StpUtil.getLoginIdAsString());
        // 根据token获取用户在线信息，过滤出有效session的用户信息
        List<UserOnlineDTO> userOnlineDTOList = tokenIds.stream()
            // 过滤出token有效期大于等于-1的token，即有效token
            .filter(token -> StpUtil.stpLogic.getTokenActiveTimeoutByToken(token) >= -1)
            // 根据token从Redis中获取用户在线信息
            .map(token -> (UserOnlineDTO) RedisUtils.getCacheObject(CacheConstants.ONLINE_TOKEN_KEY + token))
            .collect(Collectors.toList());
        // 复制和处理 SysUserOnline 对象列表
        // 数据处理：倒序排列用户在线列表，移除null元素，复制到SysUserOnline对象列表
        Collections.reverse(userOnlineDTOList);
        userOnlineDTOList.removeAll(Collections.singleton(null));
        List<SysUserOnline> userOnlineList = BeanUtil.copyToList(userOnlineDTOList, SysUserOnline.class);

        // 封装并返回用户在线列表信息
        return TableDataInfo.build(userOnlineList);
    }

    /**
     * 强退当前在线设备
     * 该方法尝试移除一个给定账号ID下的登录令牌它首先获取指定账号ID的所有令牌，
     * 然后在这些令牌中查找与给定tokenId相匹配的令牌如果找到，它将移除该令牌
     * 如果在处理过程中检测到用户未登录，则忽略异常并继续执行
     * 注意：该方法主要设计用于HTTP路径变量tokenId作为参数的情况
     *
     * @param tokenId token值 令牌ID，用于标识要移除的登录令牌
     * @return 删除操作的结果，成功返回ok，失败返回error
     */
    @Log(title = "在线设备", businessType = BusinessType.FORCE)
    @PostMapping("/{tokenId}")
    public R<Void> remove(@PathVariable("tokenId") String tokenId) {
        try {
            // 获取指定账号 id 的 token 集合
            List<String> keys = StpUtil.getTokenValueListByLoginId(StpUtil.getLoginIdAsString());
            // 从键集合中筛选出与令牌ID匹配的键，并准备进行注销操作
            keys.stream()
                .filter(key -> key.equals(tokenId))
                .findFirst()
                .ifPresent(key -> StpUtil.kickoutByTokenValue(tokenId));
        } catch (NotLoginException ignored) {
            // 如果用户未登录，则忽略异常
        }
        return R.ok();
    }

}
